#!/usr/bin/env python

################################################################################
#   Copyright 2010 Jason Hoover
#
#
#   This file is part of AutoCache.
#
#   AutoCache is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   AutoCache is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU Lesser General Public License
#   along with AutoCache.  If not, see <http://www.gnu.org/licenses/>.
################################################################################

from ac_common import *
from ac_cache import *
import logging

output=logging.getLogger('acadmin')
# create console handler and set level to debug
outputhandler = logging.StreamHandler()
outputhandler.setLevel(logging.info)

# add formatter to outputhandler
outputhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))

# add outputhandler to logger
output.addHandler(outputhandler)
output.setLevel(logging.DEBUG)

###############################################################################
# acadmin_validate - Validate, but do not update the cache object. Kind of like
# a fsck for the cache.
###############################################################################
def acadm_validate(target_list):

    # Expand all, if it exists.
    target_list = acadm_all(target_list)

    list_failed=[]

    # Validate each object in the list.
    for object in target_list:
        try:

            # Validate the object.
            validation = ac_validate(object)

            # If validation returns anything but 'true', add it to the
            # list of failed objects.
            if validation == True:
                output.info("Validation of object " + object + " succeded.")

            else:

                output.error("Validation failed for: " + object 
                             + " Error code: " + str(validation)
                             + " See log for more details.")
                list_failed.append(object)    

        # This really shouldn't happen. Validation should never raise an
        # exception unless something is REALLY wrong, such as the object
        # itself being a file, or something.
        except IOError:
            output.error("File access error validating: "+ object)

    # If there's anything in this list, the user should consider dumping it.
    acadm_purge(list_failed)

###############################################################################
# acadm_update - Updates the specified targets.
###############################################################################
def acadm_update(object_list):

    # Expand all, if it exists.
    object_list = acadm_all(object_list)

    # Initialize the variables.
    list_toupdate=[]
    list_topurge=[]
    size_toupdate=0
    size_topurge=0

    # Go through the cache looking for out of date objects.
    for object in object_list:

        if os.path.isdir( ac_settings["cache_dir"] + object + "/") or \
           os.path.isdir( ac_settings["cache_dir"] + ac_urltohash(object) ):

            try:
                 metadata = cPickle.load( file( ac_settings["cache_dir"]
                                             + object + "/metadata"))
            except IOError:
                 output.error("Problem reading metadata from: " + object)
                 continue

        # Otherwise, cue next object.
        else:
            output.error( object + " does not exist in cache, skipping..." )
            continue

        status = ac_checkup(metadata["original-url"])

        # ac_checkup codes:
        # 2, 3, 4 - Out of date. Reachable but different.
        # 1, 5, 6, 7, 8, 9 - Dead, unreachable, or broken. 
        if isinstance(status,int) and (status in (2, 3, 4)):
            list_toupdate.append(metadata["original-url"])
            size_toupdate += int(metadata["content-length"])
            output.info(object + " is out of date!")

        elif isinstance(status,int) and (status in (1, 5, 6, 7, 8, 9)):
            list_topurge.append(object)
            size_toupurge += int(metadata["content-length"])
            output.info(object + " is unreachable.")

        else:
            output.debug(object + " is current...")
               
    # Part two, if anything needs updating, do it.
    if len(list_toupdate) > 0:
    
        total_toupdate = len(list_toupdate)
        output.info( str(total_toupdate) + " out of date objects totalling "
                     + str(size_toupdate/1024) + "k" )

        if confirm("Proceed with updates"):

            # Enqueing updates is better rather than downloading them to 
            # better handle the action on a live system.
            output.info("Enqueuing updates...")
            while len(list_toupdate) > 0:
                ac_queue(list_toupdate.pop())

        else:
            output.info("User changed their mind, aborting.")

    # Don't forget to see if the user wants to purge unreachables.
    if len(list_topurge) > 0:

        output.info( str(len(list_topurge)) 
                     + " uncheckable objects, totaling "
                     + str(size_toupdate/1024) + "k" )

        if confirm("Purge uncheckable objects"):
            output.info("Purging...")
            acadm_purge(list_failed)

    output.info("Process complete, all done!")
    return 0
        

###############################################################################
# acadm_cleanup - Searches cache for entries which no longer appear in targets.
###############################################################################
def acadm_cleanup(object_list):

    # Expand 'all', if it exists.
    object_list = acadm_all(object_list)

    targets = ac_compiletargets()
    list_tocleanup = []
    size_tocleanup = 0

    for object in object_list:

        # If it looks like a URL, set a dummy metadata entry.
        if '/' in object:

           metadata = { "original-url" : object, }

        else:
            try:
                metadata = cPickle.load( file( ac_settings["cache_dir"]
                                        + object + "/metadata"))
            except IOError:
                output.error("File access error validating: "+ object)
                continue

        # Move on to the next one if we got a match.
        if targets.match(metadata["original-url"]):
            continue
            
        else:
            output.info(object  + " [" + metadata["original-url"][7:32] 
                        + "] did not match any targets.")
            list_tocleanup.append(object)
            size_tocleanup += int(metadata["content-length"])

    # If there's nothing to clean up, we're done!
    if len(list_tocleanup) > 0:

        output.info(str(len(list_tocleanup))
                    + " non-matched objects taking approximately " 
                    + str(size_tocleanup/1024) + "k of space.")
        acadm_purge(list_tocleanup)
        return 0



###############################################################################
# acadm_purge - Purges failed or specified updates, prompts. Takes URLs in a
# list, a string, or a tuple. Hell, probably even a dict.
###############################################################################
def acadm_purge(list_purge):

    # veryfailed - Updates that could not be purged for some reason.
    veryfailed = []

    if len(list_purge) > 0 and confirm(str(len(list_purge))
                                       + " objects to purge, continue"):

        # Okay, purge ahoy!
        for object in list_purge:
            try:
                output.debug("Purging object: " + object)
                ac_remove(ac_settings["cache_dir"] + object)
            except OSError:
                output.error("Unable to remove object. Carrying on. :" + object)
                veryfailed.append(object)

        # Let the user know which updates failed.
        if len(veryfailed) > 0:
            output.error("Unable to purge the following updates:")
            for failure in veryfailed:
                output.error("\t" + str(failure))

###############################################################################
# acadm_insert - Injects URLs into the cache. Takes a list.
###############################################################################

def acadm_insert(urls):

    output.info("Going to insert " + str(len(urls)) + " into queue.")
    if confirm("Insert URLs?"):
        for url in urls:
            ac_queue(url)

###############################################################################
# confirm - A simple y/n prompt. That's all.
###############################################################################
def confirm(question):

    sys.stdout.write(question+" (y/n)? ")
    response = sys.stdin.read(2).lower()

    # Used two characters (y<newline>) because just reading one causes the 
    # prompt to read an entry of y, immediately followed by an entry of a 
    # newline. This keeps it from displaying the prompt twice for each.

    while response not in ("y\n","n\n"):
        print(response)
        response = sys.stdin.read(2).lower()
        sys.stdout.write("Invalid response.\n" + question + " (y/n)? ")

    return {'y\n': True, 'n\n': False}[response]


###############################################################################
# acadm_all - Converts the word 'all' in it's argument to an expanded list of
# all cache directories. Made this into a function because I was doing it
# in several different places.
###############################################################################
def acadm_all(list):

    # If all, list the cache directory and add it to the list of items
    # to check.
    if "all" in list:
        for object in os.listdir(ac_settings["cache_dir"]):
            list.append(object)

        # 'all' isn't a valid object.
        list.remove('all')

    return list


###############################################################################
# main - Obviously.
###############################################################################
def main(args):

    # Define valid options (presently none.)
    valid_options = ['--debug','--quiet','--config','-c']

    # Define valid commands.
    valid_commands = [ 'update', 'validate', 'purge', 'insert', 'cleanup' ]

    # Define the arguments here.
    arguments = {
                'objects' : [],
                'settings': [],  
                'unparsed': [],
                'command' : '',
                }

    for entry in args:

        # All options start with -- and are listed in valid_options.
        if entry in valid_options and (valid_options.count(entry) > 0):
            arguments['settings'].append(entry)

        # No error checking done on object specs yet.
        elif len(entry) == 32 or entry == "all":
            arguments['objects'].append(entry)

        # URLs
        elif ":" in entry:
            arguments['objects'].append(entry)

        # If it's not a URL, an option, or an object, it's a command.
        elif arguments['command'] == '' and (valid_commands.count(entry) > 0):
            arguments['command'] = entry

        else:
            arguments['unparsed'].append(entry)

    if "--debug" in arguments['settings']:
        outputhandler.setLevel(logging.DEBUG)

    # Now to figure out what to do..
    if arguments['command'] == 'update':
        acadm_update(arguments['objects'])

    elif arguments['command'] == 'validate':
        acadm_validate(arguments['objects'])

    elif arguments['command'] == 'cleanup':
        acadm_cleanup(arguments['objects'])

    elif arguments['command'] == 'insert':
        acadm_insert(arguments['objects'])

    else:
        output.error("No valid command issued.")
        return 1

try:
    main(sys.argv)

except KeyboardInterrupt:
    output.info("Got ctrl-c, quitting!")
